/* Octopus PDF - A simple document viewer for Android.
   Copyright (C) 2013  Gerald Thaler

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
package de.geraldthaler.octopuspdf.control;

import static android.opengl.GLES10.GL_FLOAT;
import static android.opengl.GLES10.GL_RGBA;
import static android.opengl.GLES10.GL_TEXTURE_2D;
import static android.opengl.GLES10.GL_TEXTURE_COORD_ARRAY;
import static android.opengl.GLES10.GL_TRIANGLE_STRIP;
import static android.opengl.GLES10.GL_UNSIGNED_BYTE;
import static android.opengl.GLES10.GL_VERTEX_ARRAY;
import static android.opengl.GLES10.glBindTexture;
import static android.opengl.GLES10.glColor4f;
import static android.opengl.GLES10.glDisable;
import static android.opengl.GLES10.glDisableClientState;
import static android.opengl.GLES10.glDrawArrays;
import static android.opengl.GLES10.glEnable;
import static android.opengl.GLES10.glEnableClientState;
import static android.opengl.GLES10.glTexCoordPointer;
import static android.opengl.GLES10.glTexSubImage2D;
import static android.opengl.GLES10.glVertexPointer;
import static de.geraldthaler.octopuspdf.Constants.TEXTURE_SIZE;
import static java.lang.Math.max;
import static java.lang.Math.min;

import java.nio.ByteBuffer;

import de.geraldthaler.octopuspdf.Native;
import de.geraldthaler.octopuspdf.docrender.DocRenderTask;
import de.geraldthaler.octopuspdf.gfx.Rect;
import de.geraldthaler.octopuspdf.gfx.Size;
import de.geraldthaler.octopuspdf.memory.DirectByteBufferPool;
import de.geraldthaler.octopuspdf.memory.Disposable;
import de.geraldthaler.octopuspdf.view.TexturePool;

/**
 * @author Gerald Thaler
 */
public final class Mesh implements Disposable {
    private static final int SIZEOF_FLOAT = 4;
    private static final int SIZEOF_VERTEX = SIZEOF_FLOAT * 2;
    private static final int SIZEOF_QUAD = SIZEOF_VERTEX * 4;
    private static final int TILE_SIZE = TEXTURE_SIZE - 2;
    private static final float TEXTURE_COORD_SCALE = 1f / TEXTURE_SIZE;

    private static void addCoords(final ByteBuffer array, final float left, final float top, final float right,
            final float bottom) {
        // v1: left top
        array.putFloat(left);
        array.putFloat(top);
        // v2: left bottom
        array.putFloat(left);
        array.putFloat(bottom);
        // v3: right top
        array.putFloat(right);
        array.putFloat(top);
        // v4: right bottom
        array.putFloat(right);
        array.putFloat(bottom);
    }

    private final DirectByteBufferPool directByteBufferPool;
    private final TexturePool texturePool;
    private final int width, height;
    private final ByteBuffer textureCoords;
    private final int[] textures;
    private final int untexturedQuads;
    private final ByteBuffer vertices;
    
    public Mesh(final DirectByteBufferPool directByteBufferPool) {
        this.directByteBufferPool = directByteBufferPool;
        texturePool = null;
        this.width = 1;
        this.height = 1;
        textureCoords = null;
        textures = null;
        untexturedQuads = 1;
        vertices = directByteBufferPool.allocate(SIZEOF_QUAD);
        addCoords(vertices, 0f, 0f, width, height);
    }

    public Mesh(final TexturePool texturePool, final DirectByteBufferPool directByteBufferPool,
            final DocRenderTask[] tasks, final long[] pixmaps) {
        this.texturePool = texturePool;
        this.directByteBufferPool = directByteBufferPool;
        int width = 0, height = 0;
        int left = Integer.MAX_VALUE, top = Integer.MAX_VALUE;
        int right = 0, bottom = 0;
        for (final DocRenderTask task: tasks) {
            final Rect region = task.getRegion(); 
            left = min(left, width + region.getLeft());
            top = min(top, region.getTop());
            right = max(right, width + region.getRight());
            bottom = max(bottom, region.getBottom());
            final Size size = task.getSize();
            width += size.getWidth();
            height = size.getHeight();
        }
        this.width = width;
        this.height = height;
        untexturedQuads = 0;
        final int tilesX = (right - left + TILE_SIZE - 1) / TILE_SIZE;
        final int tilesY = (bottom - top + TILE_SIZE - 1) / TILE_SIZE;
        final int tiles = tilesX * tilesY;
        textures = texturePool.allocTextures(tiles);
        for (int n = 0, x = 0; n < tasks.length; ++n) {
            final DocRenderTask task = tasks[n];
            final Rect region = task.getRegion();
            blit(pixmaps[n], tilesX, tilesY, x + region.getLeft() - left, region.getTop() - top,
                    x + region.getRight() - left, region.getBottom() - top);
            x += task.getSize().getWidth();
        }
        vertices = directByteBufferPool.allocate(SIZEOF_QUAD * tiles);
        textureCoords = directByteBufferPool.allocate(SIZEOF_QUAD * tiles);
        for (int vt = top; vt < bottom; vt += TILE_SIZE) {
            final int vb = min(vt + TILE_SIZE, bottom);
            final int ty = 1 + vb - vt;
            for (int vl = left; vl < right; vl += TILE_SIZE) {
                final int vr = min(vl + TILE_SIZE, right);
                final int tx = 1 + vr - vl;
                addCoords(vertices, vl, vt, vr, vb);
                addCoords(textureCoords, TEXTURE_COORD_SCALE, TEXTURE_COORD_SCALE, tx * TEXTURE_COORD_SCALE,
                        ty * TEXTURE_COORD_SCALE);
            }
        }
    }

    @Override
    public void dispose() {
        directByteBufferPool.free(vertices);
        if (textureCoords != null)
            directByteBufferPool.free(textureCoords);
    }
    
    public void dispose(final int glGeneration) {
        if (textures != null)
            texturePool.freeTextures(glGeneration, textures);
        dispose();
    }

    public void draw(final float brightness) {
        glEnableClientState(GL_VERTEX_ARRAY);
        if (untexturedQuads > 0) {
            glColor4f(brightness * 0.5f, brightness * 0.5f, brightness * 0.5f, 1f);
            glDisable(GL_TEXTURE_2D);
            vertices.position(0);
            glVertexPointer(2, GL_FLOAT, 0, vertices);
            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4 * untexturedQuads);
        }
        if (textures != null) {
            glColor4f(brightness, brightness, brightness, 1f);
            glEnable(GL_TEXTURE_2D);
            glEnableClientState(GL_TEXTURE_COORD_ARRAY);
            vertices.position(4 * untexturedQuads);
            glVertexPointer(2, GL_FLOAT, 0, vertices);
            textureCoords.position(0);
            glTexCoordPointer(2, GL_FLOAT, 0, textureCoords);
            int first = 0;
            for (final int tex: textures) {
                glBindTexture(GL_TEXTURE_2D, tex);
                glDrawArrays(GL_TRIANGLE_STRIP, first, 4);
                first += 4;
            }
            glDisableClientState(GL_TEXTURE_COORD_ARRAY);
        }
        glDisableClientState(GL_VERTEX_ARRAY);
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

    private void blit(final long pixmap, final int tilesX, final int tilesY, final int left, final int top,
            final int right, final int bottom) {
        final ByteBuffer pixels = directByteBufferPool.allocate(4 * TEXTURE_SIZE * TEXTURE_SIZE);
        int tt = -1, tb = TEXTURE_SIZE - 1;
        for (int y = 0; y < tilesY; ++y) {
            final int t = max(tt, top);
            final int b = min(tb, bottom);
            if (t < b) {
                int tl = -1, tr = TEXTURE_SIZE - 1;
                for (int x = 0; x < tilesX; ++x) {
                    final int l = max(tl, left);
                    final int r = min(tr, right);
                    if (l < r) {
                        Native.blitToTexture(pixmap, right - left, l - left, t - top, r - left, b - top, pixels);
                        glBindTexture(GL_TEXTURE_2D, textures[tilesX * y + x]);
                        glTexSubImage2D(GL_TEXTURE_2D, 0, l - tl, t - tt, r - l, b - t, GL_RGBA, GL_UNSIGNED_BYTE,
                                pixels);
                    }
                    tl += TILE_SIZE;
                    tr += TILE_SIZE;
                }
            }
            tt += TILE_SIZE;
            tb += TILE_SIZE;
        }
        directByteBufferPool.free(pixels);
    }
}
